;;; Counting Sundays

;;; Problem 19

;;; You are given the following information, but you may prefer to do
;;; some research for yourself.

;;;   - 1 Jan 1900 was a Monday.

;;;   - Thirty days has September,
;;;     April, June and November.
;;;     All the rest have thirty-one,
;;;     Saving February alone,
;;;     Which has twenty-eight, rain or shine.
;;;     And on leap years, twenty-nine.

;;;   - A leap year occurs on any year evenly divisible by 4, but not
;;;   on a century unless it is divisible by 400.

;;; How many Sundays fell on the first of the month during the
;;; twentieth century (1 Jan 1901 to 31 Dec 2000)?

;; IDEA: Using the given information, one could calculate ahead until
;; the end date in steps of 7 days, each time checking, whether it is
;; the first day of a month.

;; IDEA: Cheating: Using Guile's date functions, one could probably
;; query for each first of a month, whether or not it was a Sunday.

;; IDEA: Instead of going in days steps, one could go in variable
;; sized steps from first of a month to the first of the next
;; month. If the difference in days is divisable by 7, then it is the
;; same day as the previous first of a month.

;; IDEA: Calculate each first of the month and store it as a
;; days-from-start-date number, check how many of those modulo 7 are
;; congruent to 0.

(import
 (except (rnrs base) let-values map)
 (only (guile)
       lambda* λ
       ;; printing
       display
       simple-format
       ;; command line arguments
       command-line
       current-input-port
       current-output-port)
 ;; hash tables
 (srfi srfi-69)
 ;; vectors
 (srfi srfi-43)
 (lib debug-utils))


(define DAYS-IN-MONTH
  (list->vector
   '(31  ; January
     28  ; February
     31  ; March
     30  ; April
     31  ; May
     30  ; June
     31  ; July
     31  ; August
     30  ; September
     31  ; October
     30  ; November
     31  ; December
     )))


(define MONTH-NAMES
  (alist->hash-table
   '((0 . january)
     (1 . february)
     (2 . march)
     (3 . april)
     (4 . may)
     (5 . june)
     (6 . july)
     (7 . august)
     (8 . september)
     (9 . october)
     (10 . november)
     (11 . december))))


(define WEEKDAY-NAMES
  (alist->hash-table
   '((0 . monday)
     (1 . tuesday)
     (2 . wednesday)
     (3 . thursday)
     (4 . friday)
     (5 . saturday)
     (6 . sunday))))

(define start-date-day 0)


(define leap-year?
  (λ (year)
    (cond
     ;; "A leap year occurs on any year evenly divisible by 4, but not
     ;; on a century unless it is divisible by 400."
     [(= (remainder year 4) 0)
      (or (= (remainder year 400) 0)
          (not (= (remainder year 100) 0)))]
     [else #f])))


(define january?
  (λ (month)
    (= month 0)))

(define february?
  (λ (month)
    (= month 1)))

(define december?
  (λ (month)
    (= month 11)))


(define sunday?
  (λ (days-count)
    ;; Weeks do not change at all and always have 7 days. -> When
    ;; day-count modulo 7 is 6, then the day is a Sunday.
    (= (remainder days-count 7) 6)))


(define number-of-days-in-month
  (λ (year month)
    (cond
     [(and (february? month)
           (leap-year? year))
      29]
     [else
      (vector-ref DAYS-IN-MONTH month)])))


(define next-month
      (λ (month)
        (remainder (+ month 1)
                   (vector-length DAYS-IN-MONTH))))


(define end-date?
  (λ (year month)
    (and (= year 2001)
         (january? month))))


(define count-first-of-month-sundays
  (λ ()
    (let iter ([year 1901]
               [month 0]
               ;; Start with a Tuesday.
               [days-count 1]
               [fom-sundays-count 0])
      (debug "days count:" days-count)
      ;; Move towards 31st of December 2000, by adding the number of
      ;; days in each month of a year, so that we always move from
      ;; first of one month to first of the next month. We do not need
      ;; to look at any other days. Each iteration we need to check,
      ;; if we are at a Sunday.
      (cond
       [(end-date? year month) fom-sundays-count]
       [else
        (cond
         [(sunday? days-count)
          (displayln year month "is a" (hash-table-ref WEEKDAY-NAMES (remainder days-count 7)))
          (iter (if (december? month) (+ year 1) year)
                (next-month month)
                ;; Add number of days in this month to get to the next
                ;; first of the month.
                (+ days-count (number-of-days-in-month year month))
                (+ fom-sundays-count 1))]
         [else
          (displayln year month "is a" (hash-table-ref WEEKDAY-NAMES (remainder days-count 7)))
          (iter (if (december? month) (+ year 1) year)
                (next-month month)
                ;; Add number of days in this month to get to the next
                ;; first of the month.
                (+ days-count (number-of-days-in-month year month))
                fom-sundays-count)])]))))


(displayln "number of first of the month sundays:" (count-first-of-month-sundays))
